<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>视频监控面板</title>
  <!--
    使用说明：
    1) 后端提供接口：GET /queryCameraPreviewURLs
       期望返回 JSON，可以是以下任意一种：
       A. { cameras: [ { id, name, url }, ... ] }
       B. [ { id, name, url }, ... ]
       C. [ "https://.../live.m3u8", ... ]
       D. [ { cameraName, previewUrl }, ... ]
       脚本会尽力做字段映射；如果字段不同，请在 mapCamera() 中调整。

    2) 支持 HLS(m3u8)：
       - Safari/iOS 走原生 <video> 播放。
       - 其它浏览器使用 hls.js。请确保页面通过 HTTPS 访问，避免混合内容报错。

    3) 功能：
       - 四分屏分页浏览（每页 4 路）。
       - 单窗/整页全屏。
       - 左上角名称叠加显示。
       - 自动静音自动播放（可取消静音）。
       - 失败时重试与错误提示。
  -->
  <style>
    :root {
      --bg: #0b1015;
      --card: #121821;
      --muted: #8aa0b4;
      --accent: #2eaadc;
      --danger: #e35d6a;
      --ok: #38c172;
    }
    html, body {
      height: 100%;
      margin: 0;
      background: var(--bg);
      color: #e7edf3;
      font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
    }
    .container {
      display: flex;
      flex-direction: column;
      min-height: 100%;
    }
    header {
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 12px 16px;
      background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0));
      border-bottom: 1px solid rgba(255,255,255,0.06);
    }
    h1 { font-size: 18px; margin: 0 8px 0 0; font-weight: 600; }
    .spacer { flex: 1; }
    button, select {
      background: var(--card);
      color: #e7edf3;
      border: 1px solid rgba(255,255,255,0.08);
      border-radius: 10px;
      padding: 8px 12px;
      font-size: 14px;
      cursor: pointer;
    }
    button:hover { border-color: rgba(255,255,255,0.18); }
    button:disabled { opacity: .5; cursor: not-allowed; }
    main {
      flex: 1;
      padding: 12px;
    }
    .grid {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      grid-template-rows: repeat(2, 1fr);
      gap: 12px;
      height: calc(100vh - 78px);
      max-height: calc(100vh - 78px);
    }
    .tile {
      position: relative;
      background: var(--card);
      border: 1px solid rgba(255,255,255,0.06);
      border-radius: 16px;
      overflow: hidden;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 200px;
      box-shadow: 0 10px 30px rgba(0,0,0,.25);
    }
    .tile video {
      width: 100%;
      height: 100%;
      object-fit: contain; /* 监控画面尽量不裁切，可改为 'cover' 填满 */
      background: #000;
    }
    .badge {
      position: absolute;
      top: 10px;
      left: 10px;
      background: rgba(0,0,0,.55);
      border: 1px solid rgba(255,255,255,.12);
      backdrop-filter: blur(6px);
      border-radius: 10px;
      padding: 6px 10px;
      font-size: 13px;
      font-weight: 600;
      pointer-events: none;
      max-width: 70%;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .controls {
      position: absolute;
      right: 10px;
      bottom: 10px;
      display: flex;
      gap: 8px;
    }
    .chip {
      background: rgba(0,0,0,.55);
      border: 1px solid rgba(255,255,255,.12);
      border-radius: 10px;
      padding: 6px 10px;
      font-size: 12px;
      cursor: pointer;
      user-select: none;
    }
    .status {
      position: absolute;
      left: 10px;
      bottom: 10px;
      font-size: 12px;
      color: var(--muted);
      display: flex;
      gap: 8px;
      align-items: center;
    }
    .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
    .dot.ok { background: var(--ok); }
    .dot.err { background: var(--danger); }
    footer {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 10px 16px 16px 16px;
      color: var(--muted);
    }
    .page-indicator { margin-left: 8px; }
    .link { color: var(--accent); text-decoration: none; }
    .link:hover { text-decoration: underline; }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
</head>
<body>
  <div class="container">
    <header>
      <h1>视频监控面板</h1>
      <button id="btn-refresh" title="重新拉取后端列表">刷新</button>
      <div class="spacer"></div>
      <button id="btn-prev">上一页</button>
      <span class="page-indicator" id="page-text">第 1 / 1 页</span>
      <button id="btn-next">下一页</button>
      <button id="btn-fullgrid">整页全屏</button>
      <select id="fit-mode" title="画面适配">
        <option value="contain" selected>适配(不裁切)</option>
        <option value="cover">铺满(可能裁切)</option>
      </select>
    </header>

    <main>
      <div class="grid" id="grid"></div>
    </main>

    <!-- <footer>
      接口：<code>/queryCameraPreviewURLs</code> · 若需自定义字段，请修改 <code>mapCamera()</code>。
      示例地址：<a class="link" href="/hikview/openUrl/6itXG0w/live.m3u8" target="_blank" rel="noreferrer">m3u8 示例</a>
    </footer> -->
  </div>

  <script>
    /**
     * —— 数据映射 ——
     * 将任意返回结构映射为 [{ id, name, url }]
     */
    function mapCamera(raw) {
      if (!raw) return [];
      let list = [];
      // A: { cameras: [...] }
      if (raw.data && Array.isArray(raw.cameras)) list = raw.data;
      // B: [ {...} ] 或 C: [ "url" ]
      else if (Array.isArray(raw)) list = raw;
      else if (raw.data && Array.isArray(raw.data)) list = raw.data; // 兜底

      return list.map((item, idx) => {
        const id = item.cameraIndexCode;
        const name = item.cameraName;
        const url = item.viewUrl;
        return { id: String(id), name, url };
      }).filter(x => !!x.url);
    }

    /**
     * —— HLS 附着与销毁管理 ——
     */
    const hlsMap = new Map(); // videoEl -> hls instance

    function attachStream(videoEl, url) {
      cleanupVideo(videoEl);
      // iOS/Safari 原生支持 HLS
      if (videoEl.canPlayType('application/vnd.apple.mpegurl')) {
        videoEl.src = url;
        videoEl.play().catch(() => {});
        return null;
      }
      if (window.Hls && Hls.isSupported()) {
        const hls = new Hls({ enableWorker: true, lowLatencyMode: true });
        hls.loadSource(url);
        hls.attachMedia(videoEl);
        hls.on(Hls.Events.ERROR, (_, data) => {
          if (data.fatal) {
            videoEl.dataset.status = 'error';
          }
        });
        hlsMap.set(videoEl, hls);
        return hls;
      } else {
        // 不支持 HLS 且非 Safari，尝试直接播放（部分浏览器可能支持）
        videoEl.src = url;
        videoEl.play().catch(() => {});
        return null;
      }
    }

    function cleanupVideo(videoEl) {
      try {
        const hls = hlsMap.get(videoEl);
        if (hls) { hls.destroy(); hlsMap.delete(videoEl); }
        videoEl.pause();
        videoEl.removeAttribute('src');
        videoEl.load();
      } catch (e) {}
    }

    function cleanupAll() {
      for (const [el, hls] of hlsMap.entries()) {
        try { hls.destroy(); } catch (e) {}
      }
      hlsMap.clear();
      document.querySelectorAll('video').forEach(v => cleanupVideo(v));
    }

    /**
     * —— 分页与渲染 ——
     */
    const state = {
      cameras: [],
      page: 0,
      pageSize: 4,
    };

    function pageCount() {
      return Math.max(1, Math.ceil(state.cameras.length / state.pageSize));
    }

    function updatePager() {
      const txt = document.getElementById('page-text');
      txt.textContent = `第 ${Math.min(state.page+1, pageCount())} / ${pageCount()} 页`;
      document.getElementById('btn-prev').disabled = state.page <= 0;
      document.getElementById('btn-next').disabled = state.page >= pageCount()-1;
    }

    function renderPage() {
      cleanupAll();
      const grid = document.getElementById('grid');
      grid.innerHTML = '';

      const begin = state.page * state.pageSize;
      const slice = state.cameras.slice(begin, begin + state.pageSize);

      slice.forEach((cam, i) => {
        const tile = document.createElement('div');
        tile.className = 'tile';
        tile.dataset.camId = cam.id;

        const badge = document.createElement('div');
        badge.className = 'badge';
        badge.textContent = cam.name || `通道${begin + i + 1}`;
        // tile.appendChild(badge);

        const video = document.createElement('video');
        video.autoplay = true;
        video.muted = true; // 自动播放政策
        video.playsInline = true;
        video.controls = true;
        video.preload = 'auto';
        video.dataset.status = 'loading';
        video.addEventListener('loadeddata', () => video.dataset.status = 'ok');
        video.addEventListener('error', () => video.dataset.status = 'error');
        tile.appendChild(video);

        const controls = document.createElement('div');
        controls.className = 'controls';
        const btnFull = document.createElement('div');
        btnFull.className = 'chip';
        btnFull.textContent = '全屏';
        btnFull.onclick = () => requestFullscreen(tile);
        const btnRetry = document.createElement('div');
        btnRetry.className = 'chip';
        btnRetry.textContent = '重试';
        btnRetry.onclick = () => attachStream(video, cam.url);
        const btnMute = document.createElement('div');
        btnMute.className = 'chip';
        btnMute.textContent = '静音';
        btnMute.onclick = () => { video.muted = !video.muted; btnMute.textContent = video.muted ? '静音' : '外放'; };
        // controls.appendChild(btnFull);
        controls.appendChild(btnRetry);
        // controls.appendChild(btnMute);
        tile.appendChild(controls);

        const status = document.createElement('div');
        status.className = 'status';
        const dot = document.createElement('span');
        dot.className = 'dot';
        const txt = document.createElement('span');
        txt.textContent = '连接中...';
        status.appendChild(dot);
        status.appendChild(txt);
        tile.appendChild(status);

        // 状态变化指示
        const updateStatus = () => {
          if (video.dataset.status === 'ok') { dot.className = 'dot ok'; txt.textContent = '已连接'; }
          else if (video.dataset.status === 'error') { dot.className = 'dot err'; txt.textContent = '错误'; }
          else { dot.className = 'dot'; txt.textContent = '连接中...'; }
        };
        const ob = new MutationObserver(updateStatus);
        ob.observe(video, { attributes: true, attributeFilter: ['data-status'] });

        grid.appendChild(tile);
        attachStream(video, cam.url);
      });

      updatePager();
      applyFitMode();
    }

    /**
     * —— 全屏 ——
     */
    function requestFullscreen(el) {
      if (el.requestFullscreen) el.requestFullscreen();
      else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
      else if (el.msRequestFullscreen) el.msRequestFullscreen();
    }

    document.getElementById('btn-fullgrid').addEventListener('click', () => requestFullscreen(document.documentElement));

    /**
     * —— 画面适配 ——
     */
    function applyFitMode() {
      const val = document.getElementById('fit-mode').value;
      document.querySelectorAll('.tile video').forEach(v => v.style.objectFit = val);
    }
    document.getElementById('fit-mode').addEventListener('change', applyFitMode);

    /**
     * —— 分页按钮 ——
     */
    document.getElementById('btn-prev').addEventListener('click', () => {
      if (state.page > 0) { state.page--; renderPage(); }
    });
    document.getElementById('btn-next').addEventListener('click', () => {
      if (state.page < pageCount()-1) { state.page++; renderPage(); }
    });

    /**
     * —— 拉取后端 ——
     */
    async function fetchCameras() {
      const btn = document.getElementById('btn-refresh');
      btn.disabled = true; btn.textContent = '加载中...';
      try {
        const res = await fetch('http://192.168.50.12/queryCameraPreviewURLs', { cache: 'no-store' });
        if (!res.ok) throw new Error('网络错误: ' + res.status);
        const raw = await res.json();
        state.cameras = mapCamera(raw);
        if (!state.cameras.length) throw new Error('未获取到摄像头地址');
        state.page = 0;
        renderPage();
      } catch (e) {
        console.error(e);
        alert('加载失败：' + (e.message || e));
      } finally {
        btn.disabled = false; btn.textContent = '刷新';
      }
    }

    document.getElementById('btn-refresh').addEventListener('click', fetchCameras);

    // 页面可直接演示：若后端不可用，使用临时示例（可删除）
    async function demoFallback() {
      // 如需展示，将 demoEnabled 设为 true
      const demoEnabled = false;
      if (!demoEnabled) return;
      const demoList = [
        { name: '样例通道-1', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' },
        { name: '样例通道-2', url: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8' },
        { name: '样例通道-3', url: 'https://moq-proxy.akamaized.net/live/eds/nbcnewsfmc1-hls/playlist.m3u8' },
        { name: '样例通道-4', url: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8' },
        { name: '样例通道-5', url: 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8' },
      ];
      state.cameras = demoList;
      renderPage();
    }

    // 首次加载
    fetchCameras().catch(() => demoFallback());

    // 资源清理：页面关闭/刷新时
    window.addEventListener('beforeunload', cleanupAll);
  </script>
</body>
</html>
